/*
 * Copyright (c) 2020-2099 the original author or authors. All rights reserve.
 */
package org.tianyun.cloud.config.refresh.smart;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.scope.ScopedProxyUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.cloud.context.scope.GenericScope;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.MethodMetadata;
import org.tianyun.cloud.utils.RegExUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * 带条件的刷新Scope，监听Environment配置变动，配置变动后销毁指定Bean，根据prefix匹配。
 *
 * <p>
 * 可通过两种方式来初始化： 1、直接在Bean配置上加@SmartScope； 2、使用SmartBeanDefinition来手动注册Bean
 *
 * @auther ebert_chan
 */
public class SmartScope extends GenericScope implements ApplicationContextAware, ApplicationListener<ContextRefreshedEvent>, Ordered {

    private final static Logger LOGGER = LoggerFactory.getLogger(SmartScope.class);


    public static final String SCOPE_NAME = "smart";

    private ApplicationContext context;

    private BeanDefinitionRegistry registry;

    private boolean eager = true;

    private int order = Ordered.LOWEST_PRECEDENCE - 99;

    private final SmartScopeBeanWrapperCache beanWrapperCache;

    /**
     * Creates a scope instance and gives it the default name: "smart".
     */
    public SmartScope() {
        super.setName(SCOPE_NAME);
        this.beanWrapperCache = new SmartScopeBeanWrapperCache();
    }

    /*
     * @see org.springframework.core.Ordered#getOrder()
     */
    @Override
    public int getOrder() {
        return this.order;
    }

    public void setOrder(int order) {
        this.order = order;
    }

    /**
     * Flag to determine whether all beans in refresh scope should be instantiated eagerly on startup. Default true.
     *
     * @param eager The flag to set.
     */
    public void setEager(boolean eager) {
        this.eager = eager;
    }

    @Override
    public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException {
        this.registry = registry;
        super.postProcessBeanDefinitionRegistry(registry);
        this.resolveRefreshBean();
    }

    private void resolveRefreshBean() {
        for (String name : registry.getBeanDefinitionNames()) {
            BeanDefinition definition = registry.getBeanDefinition(name);
            if (!getName().equals(definition.getScope()) || !ScopedProxyUtils.isScopedTarget(name)) {
                continue;
            }

            String prefix = null;
            if (definition instanceof SmartBeanDefinition) {
                SmartBeanDefinition refreshDef = (SmartBeanDefinition) definition;
                prefix = refreshDef.getRefreshKey();
            } else if (definition instanceof AnnotatedBeanDefinition) {
                AnnotatedBeanDefinition annDef = (AnnotatedBeanDefinition) definition;
                MethodMetadata methodMetadata = annDef.getFactoryMethodMetadata();
                AnnotationAttributes attributes = AnnotationAttributes.fromMap((methodMetadata != null ? methodMetadata : annDef.getMetadata()).getAnnotationAttributes(org.tianyun.cloud.config.refresh.smart.annotation.SmartScope.class.getName(), false));
                if (attributes != null) {
                    prefix = attributes.getString("prefix");
                }
            }

            if (prefix == null) {
                prefix = StringUtils.EMPTY;
            }

            SmartScopeBeanWrapper wrapper = new SmartScopeBeanWrapper(prefix, name);
            beanWrapperCache.add(wrapper);
        }

        beanWrapperCache.getBeanWrappers().stream().collect(Collectors.groupingBy(SmartScopeBeanWrapper::getRefreshKey, Collectors.toList())).forEach((key, value) -> {
            LOGGER.info("Smart scope refresh beans: [{}] -> [{}]", StringUtils.isEmpty(key) ? StringUtils.EMPTY : key, value.stream().map(wrapper -> ScopedProxyUtils.getOriginalBeanName(wrapper.getBeanName())).collect(Collectors.joining(", ")));
        });

    }

    /*
     * @see org.springframework.context.ApplicationListener#onApplicationEvent(org.springframework.context.ApplicationEvent)
     */
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        start(event);
    }

    public void start(ContextRefreshedEvent event) {
        if (event.getApplicationContext() == this.context && this.eager && this.registry != null) {
            eagerlyInitialize();
        }
    }

    private void eagerlyInitialize() {
        for (String name : this.context.getBeanDefinitionNames()) {
            BeanDefinition definition = this.registry.getBeanDefinition(name);
            if (this.getName().equals(definition.getScope()) && !definition.isLazyInit()) {
                Object bean = this.context.getBean(name);
                if (bean != null) {
                    bean.getClass();
                }
            }
        }
    }

    public synchronized void refreshAll() {
        Set<String> refreshedKeys = new HashSet<>();
        for (SmartScopeBeanWrapper beanWrapper : beanWrapperCache.getBeanWrappers()) {
            if (StringUtils.isNotBlank(beanWrapper.getRefreshKey())) {
                refreshedKeys.add(beanWrapper.getRefreshKey());
            }
            super.destroy(beanWrapper.getBeanName());
        }

        if (this.eager && this.registry != null) {
            try {
                eagerlyInitialize();
            } catch (Exception e) {
                LOGGER.error("Failed to eagerly initialize bean.", e);
            }
        }

        this.context.publishEvent(new SmartScopeRefreshedEvent(refreshedKeys));
    }

    public synchronized void refresh(Set<String> keys) {
        Set<String> refreshedKeys = new HashSet<>();
        Set<KeyPattern> keyPatternSet = keys.stream().map(KeyPattern::new).collect(Collectors.toSet());

        for (SmartScopeBeanWrapper beanWrapper : beanWrapperCache.getBeanWrappers()) {

            boolean isMacher = Boolean.FALSE;
            for (KeyPattern keyPattern : keyPatternSet) {
                if (keyPattern.isMatcher(beanWrapper.getRefreshKey())) {
                    isMacher = Boolean.TRUE;
                    break;
                }
            }

            if (!isMacher) {
                continue;
            }

            if (StringUtils.isNotBlank(beanWrapper.getRefreshKey())) {
                refreshedKeys.add(beanWrapper.getRefreshKey());
            }

            super.destroy(beanWrapper.getBeanName());

        }

        if (this.eager && this.registry != null) {
            try {
                eagerlyInitialize();
            } catch (Exception e) {
                LOGGER.error("Failed to eagerly initialize bean.", e);
            }
        }

        this.context.publishEvent(new SmartScopeRefreshedEvent(refreshedKeys));
    }

    @Override
    public void setApplicationContext(ApplicationContext context) throws BeansException {
        this.context = context;
    }

    public static class SmartScopeBeanWrapperCache {

        private final List<SmartScopeBeanWrapper> beanWrappers;

        public SmartScopeBeanWrapperCache() {
            beanWrappers = new ArrayList<>();
        }

        public void add(SmartScopeBeanWrapper wapper) {
            beanWrappers.add(wapper);
        }

        public final List<SmartScopeBeanWrapper> getBeanWrappers() {
            return Collections.unmodifiableList(beanWrappers);
        }

    }

    public static class SmartScopeBeanWrapper {

        private String refreshKey;

        private String beanName;

        public SmartScopeBeanWrapper(String refreshKey, String beanName) {
            this.refreshKey = refreshKey;
            this.beanName = beanName;
        }

        public String getRefreshKey() {
            return refreshKey;
        }

        public String getBeanName() {
            return beanName;
        }

    }
/*
    public static class KeySubsections {

        private String prefix;

        private String[] subsections;

        public KeySubsections(String prefix) {
            this.prefix = prefix;
            if (!StringUtils.hasLength(prefix)) {
                return;
            }
            subsections = StringUtils.split(prefix, RegexUtils.DOT);
        }

        @Override
        public String toString() {
            return this.prefix;
        }

        public String[] getSubsections() {
            return subsections;
        }

    }*/

    public static class KeyPattern {

        private String key;

        private Boolean regex = Boolean.FALSE;

        private List<Pattern> patterns;

        public KeyPattern(String key) {
            this.key = key;

            if (!RegExUtils.containsWildcardCharacter(key)) {
                return;
            }

            regex = Boolean.TRUE;
            patterns = RegExUtils.splitWildcardCharacterToPattern(key, RegExUtils.DOT);
        }

        public boolean isMatcher(String text) {
            if (StringUtils.isEmpty(text)) {
                return Boolean.TRUE;
            }

            if (!this.regex) {
                return toString().startsWith(text);
            }

            for (int i = 0; i < patterns.size(); i++) {
                Matcher matcher = patterns.get(i).matcher(text);
                if (!matcher.find()) {
                    return Boolean.FALSE;
                }
                text = matcher.replaceFirst("");
                if (StringUtils.isEmpty(text)) {
                    break;
                }
            }

            return text.isEmpty();
        }

        @Override
        public String toString() {
            return this.key;
        }

    }

}
